/*
 * Famon (http://code.google.com/p/famon/)
 * Copyright (c) 2010 Carl Crous.
 *
 * This file is part of Famon.
 *
 * Famon is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Famon is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Famon.  If not, see <http://www.gnu.org/licenses/>.
 */

package za.org.foobar.famon.reporter;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.LogManager;
import za.org.foobar.famon.Serializer;
import za.org.foobar.famon.dto.*;
import za.org.foobar.famon.util.Util;
import za.org.foobar.famon.util.ValueGetter;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 */
public class FamonReporterLogic {
    private final Log log = LogFactory.getLog(getClass());

    private String server = null;
    private String application = null;
    private String instance = null;
    private ValueGetter user = null;
    private ValueGetter version = null;
    private boolean sendIpAndHostname = true;
    private int serverTimeoutSeconds = 30;
    private int failureHistorySize = 5;
    private boolean excludeDuplicates = true;
    private int duplicateThresholdMs = 1000;

    private FamonWorkProcessor workProcessor;
    private InetAddress localMachine;
    private List<Pattern> excludePatterns = new ArrayList<Pattern>();
    private FamonAppender log4jAppender;
    private LinkedList<CFailure> failureHistory = new LinkedList<CFailure>();

    public FamonReporterLogic() {
    }

    public FamonWorkProcessor getWorkProcessor() {
        return workProcessor;
    }

    public void setWorkProcessor(FamonWorkProcessor workProcessor) {
        workProcessor.setFamonReporterLogic(this);
        this.workProcessor = workProcessor;
    }

    public void setWorkProcessorByName(String simpleName) {
        if (simpleName.equalsIgnoreCase(FamonThreadPoolWorkProcessor.class.getSimpleName())) {
            setWorkProcessor(new FamonThreadPoolWorkProcessor());
        }
        if (simpleName.equalsIgnoreCase(FamonSynchronousWorkProcessor.class.getSimpleName())) {
            setWorkProcessor(new FamonSynchronousWorkProcessor());
        }
        if (simpleName.equalsIgnoreCase(FamonFileBufferWorkProcessor.class.getSimpleName())) {
            setWorkProcessor(new FamonFileBufferWorkProcessor());
        }
    }

    public List<Pattern> getExcludePatterns() {
        return excludePatterns;
    }

    public void setExcludePatterns(List<Pattern> excludePatterns) {
        this.excludePatterns = excludePatterns;
    }

    public void clearExcludePatterns() {
        excludePatterns.clear();
    }

    public void addExcludeRegex(String regex) {
        addExcludePattern(Pattern.compile(regex));
    }

    public void addExcludePattern(Pattern pattern) {
        excludePatterns.add(pattern);
    }

    public String getServer() {
        return server;
    }

    public void setServer(String server) {
        log.debug("server = \"" + server + "\"");
        this.server = Util.emptyAsNull(server);
    }

    public boolean isEnabled() {
        return server != null;
    }

    public String getApplicationAddress() {
        String s = server;
        int i = s.lastIndexOf("/collector");
        if (i > 0) {
            s = s.substring(0, i+1);
        }
        try {
            return s + "failure_list.jsp?application_name=" + URLEncoder.encode(application, "UTF-8") +
                    "&instance_name=" + URLEncoder.encode(getInstance(), "UTF-8") +
                    "&version_name=" + URLEncoder.encode(version.getValue(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public String getApplication() {
        return application;
    }

    public void setApplication(String application) {
        log.debug("application = \"" + application + "\"");
        this.application = Util.emptyAsNull(application);
    }

    public String getInstance() {
        return instance;
    }

    public void setInstance(String instance) {
        log.debug("instance = \"" + instance + "\"");
        this.instance = Util.emptyAsNull(instance);
    }

    public void setUserValue(String userValue) {
        log.debug("userValue = \"" + userValue + "\"");
        if (user == null) {
            user = new ValueGetter(userValue, null);
        } else {
            user.setValue(userValue);
        }
    }

    public void setUserGetter(String userGetter) {
        log.debug("userGetter = \"" + userGetter + "\"");
        if (user == null) {
            user = new ValueGetter(null, userGetter);
        } else {
            user.setGetter(userGetter);
        }
    }

    public void setVersionValue(String versionValue) {
        log.debug("versionValue = \"" + versionValue + "\"");
        if (version == null) {
            version = new ValueGetter(versionValue, null);
        } else {
            version.setValue(versionValue);
        }
    }

    public void setVersionGetter(String versionGetter) {
        log.debug("versionGetter = \"" + versionGetter + "\"");
        if (version == null) {
            version = new ValueGetter(null, versionGetter);
        } else {
            version.setGetter(versionGetter);
        }
    }

    public boolean isSendIpAndHostname() {
        return sendIpAndHostname;
    }

    public void setSendIpAndHostname(boolean sendIpAndHostname) {
        log.debug("sendIpAndHostname = \"" + sendIpAndHostname + "\"");
        this.sendIpAndHostname = sendIpAndHostname;
        if (sendIpAndHostname && localMachine == null) {
            try {
                localMachine = InetAddress.getLocalHost();
            } catch (java.net.UnknownHostException uhe) {
                log.warn("Could not determine hostname using java.net.InetAddress.getLocalHost()");
            }
        }
    }

    public int getServerTimeoutSeconds() {
        return serverTimeoutSeconds;
    }

    public void setServerTimeoutSeconds(int serverTimeoutSeconds) {
        log.debug("serverTimeoutSeconds = \"" + serverTimeoutSeconds + "\"");
        this.serverTimeoutSeconds = serverTimeoutSeconds;
    }

    public boolean isLog4jAppenderEnabled() {
        return log4jAppender != null;
    }

    public int getLog4jAppenderLoggingEventsPerThread() {
        return log4jAppender.getLoggingEventsPerThread();
    }

    public void setLog4jAppender(int loggingEventsPerThread) {
        if (!isEnabled() || loggingEventsPerThread <= 0) {
            if (log4jAppender != null) {
                removeLog4jAppender();
            }
            return;
        }
        if (log4jAppender == null) {
            log4jAppender = new FamonAppender();
            LogManager.getRootLogger().addAppender(log4jAppender);
        }
        log4jAppender.setLoggingEventsPerThread(loggingEventsPerThread);
    }

    public void removeLog4jAppender() {
        LogManager.getRootLogger().removeAppender(log4jAppender);
        log4jAppender = null;
    }

    public int getFailureHistorySize() {
        return failureHistorySize;
    }

    public void setFailureHistorySize(int failureHistorySize) {
        log.debug("failureHistorySize = \"" + failureHistorySize + "\"");
        this.failureHistorySize = failureHistorySize;
    }

    public Deque<CFailure> getFailureHistory() {
        return failureHistory;
    }

    public CFailure getLastFailure() {
        return failureHistory.peekLast();
    }

    public boolean isExcludeDuplicates() {
        return excludeDuplicates;
    }

    public void setExcludeDuplicates(boolean excludeDuplicates) {
        log.debug("excludeDuplicates = \"" + excludeDuplicates + "\"");
        this.excludeDuplicates = excludeDuplicates;
    }

    public int getDuplicateThresholdMs() {
        return duplicateThresholdMs;
    }

    public void setDuplicateThresholdMs(int duplicateThresholdMs) {
        log.debug("duplicateThresholdMs = \"" + duplicateThresholdMs + "\"");
        this.duplicateThresholdMs = duplicateThresholdMs;
    }

    /**
     * Reports a failure to the Famon server. Does nothing if the server has not been set.
     * This method is not synchronized and the caller must make sure that calls to this
     * are synchronized.
     * @param mainException The exception to report.
     */
    public void fail(Throwable mainException) {
        // If the server is missing, disable reporting.
        if (!isEnabled()) {
            log.debug("No server is set, failure reporting is disabled");
            return;
        }
        log.info("Fail called for Logic " + this);
        Throwable e = Util.findCause(mainException);
        //TODO: add the exceptions the main cause caused.
        if (isExcluded(e)) {
            log.info("Failure is in the exclude list and will be ignored.");
            return;
        }

        CFailure failure = createCFailure(e);

        // Check if the failure is a duplicate
        if (excludeDuplicates) {
            for (CFailure f : failureHistory) {
                if (areEqual(failure, f)) {
                    log.info("Failure is a duplicate and will be ignored.");
                    return;
                }
            }
        }

        // Save the new failure
        if (failureHistorySize > 0) {
            while (failureHistory.size() + 1 > failureHistorySize) {
                failureHistory.remove();
            }
            failureHistory.add(failure);
        }

        String threadName = Thread.currentThread().getName();
        List<CLoggingEvent> loggingEventList = isLog4jAppenderEnabled() ? log4jAppender.getLoggingEvents() : null;
        if (workProcessor == null) {
            throw new IllegalArgumentException("No work processor set");
        }
        workProcessor.sendFailure(failure, threadName, loggingEventList, e);
    }

    public boolean areEqual(Object o1, Object o2) {
        if (o1 == o2) {
            return true;
        } else if (o1 == null || o2 == null) {
            return false;
        }
        return o1.equals(o2);
    }

    public boolean areEqual(CFailure f1, CFailure f2) {
        if (f1 == f2) {
            return true;
        } else if (f1 == null || f2 == null) {
            return false;
        }
        long t1 = f1.getDate() != null ? f1.getDate().getTime() : 0;
        long t2 = f2.getDate() != null ? f2.getDate().getTime() : 0;
        return  areEqual(f1.getFailureType(), f2.getFailureType())
                && areEqual(f1.getMessage(), f2.getMessage())
                && Math.abs(t1 - t2) < duplicateThresholdMs;
    }

    private boolean isExcluded(Throwable e) {
        return isExcluded(e.toString());
    }

    private boolean isExcluded(String path) {
        log.debug("Checking ignore for: " + path);
        for (Pattern p : excludePatterns) {
            Matcher m = p.matcher(path);
            if (m.matches()) {
                log.info("Exclude matched failure: " + m.toString());
                return true;
            }
        }
        return false;
    }

    public CFailureBatch createCFailureBatch() {
        CFailureBatch batch = new CFailureBatch();
        batch.setApplication(application);
        batch.setInstance(instance);
        batch.setVersion(version.getValue());
        if (sendIpAndHostname) {
            batch.setSourceAddress(localMachine.getHostAddress());
            batch.setSourceHostname(localMachine.getHostName());
        }
        return batch;
    }

    private CFailure createCFailure(Throwable e) {
        CFailure failure = new CFailure();
        failure.setUser(user.getValue());
        failure.setMessage(e.getMessage());
        failure.setDate(new Date());
        failure.setFailureType(e.getClass().getName());
        return failure;
    }

    public void sendFailure(CFailure failure) {
        // Only going to be adding the current thread to the failure message
        // TODO: Add all threads (possibly configure this as an option)

        CFailureBatch batch = createCFailureBatch();
        batch.addFailure(failure);
        sendFailureBatch(batch);
    }

    public void sendFailureBatch(CFailureBatch batch) {
        String json = Serializer.serializeFailureBatch(batch);
        // Add the Famon version number prefix to the message
        json = (CFailureBatch.FAMON_PROTOCOL_VERSION + CFailureBatch.FAMON_PROTOCOL_VERSION_SEPARATOR) + json;
        HttpClient client = null;
        try {
            client = new HttpClient(server, json, serverTimeoutSeconds);
        } catch (IOException ioe) {
            log.error("Could not send failure report to server \"" + server + "\". Error: " + ioe.getMessage());
            if (log.isTraceEnabled()) {
                log.trace("Message: " + json);
            }
        }
        if (client != null && !client.isResponseOK()) {
            log.error("Could not send failure report to server \"" + server + "\".");
            if (log.isDebugEnabled()) {
                log.debug("Server response: " + client.getResponseCode() + " " + client.getContent());
                if (log.isTraceEnabled()) {
                    log.trace("Message: " + json);
                }
            }
        } else if (client != null) {
//            if (log.isDebugEnabled()) {
//                System.out.println("\n\n"+json+"\n\n");
//            }
            log.info("Failure sent successfully");
        }
    }

    public void populateFailure(CFailure failure, String threadName, List<CLoggingEvent> loggingEventList,
                                Throwable exception) {
        failure.addThread(getThreadDto(threadName, loggingEventList, exception.getStackTrace()));
    }
    
    public CThreadInformation getThreadDto(String threadName, List<CLoggingEvent> loggingEventList,
                                            StackTraceElement stackTrace[]) {
        CThreadInformation dto = new CThreadInformation();
        dto.setName(threadName);

        if (loggingEventList != null) {
            dto.setLoggingEvents(loggingEventList);
        }

        // Add all the stack trace elements to the thread dto
        for (StackTraceElement element : stackTrace) {
            dto.addStackTraceElement(getStackTraceElementDto(element));
        }
        
        return dto;
    }

    private CStackTraceElement getStackTraceElementDto(StackTraceElement element) {
        return new CStackTraceElement(
            element.getFileName(),
            element.getLineNumber(),
            element.getClassName(),
            element.getMethodName()
        );
    }
}
